package de.zainodis.commons.communication.local;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;

import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;

import junit.framework.AssertionFailedError;
import de.zainodis.commons.LogCat;
import de.zainodis.commons.exception.ReflectionException;
import de.zainodis.commons.utils.ReflectionUtils;

/**
 * A message port can be defined by a class, they can be subscribed to and
 * messages can be sent to them.
 * 
 * @author zainodis
 * 
 */
public class MessagePort {

   private static final String TAG = "MessagePort";

   private Class<?>[] receiverParameters;

   private Set<MessagePortSubscriber> subscribers = new HashSet<MessagePortSubscriber>();

   /**
    * In order to create a message port, that can receive messages and forward
    * them to subscribers, we need to define the method signature of the
    * receiving method. One universal rule applies
    * 
    * @param parameters
    */
   public MessagePort(Class<?>... parameters) {
	 assertNotNull("Parameters defined by a message port must not be null.", parameters);
	 receiverParameters = parameters;
   }

   /**
    * Subscribes the given {@link MessagePortSubscriber} to messages sent by
    * this {@link MessagePort}.
    * 
    * @param subscriber
    *           the new subscriber.
    * @throws AssertionFailedError
    *            if either the subscriber is null or the subscriber's receiving
    *            method's signature does not match the signature defined by this
    *            message port.
    */
   public void subscribe(MessagePortSubscriber subscriber) throws AssertionFailedError {

	 assertNotNull("MessagePortSubscriber must not be null when subscribing to a message port.",
		  subscriber);
	 /*
	  * Ensure, that the signature of the subscriber's receiver method matches
	  * the signature defined by this port.
	  */
	 Method method = null;
	 try {
	    method = ReflectionUtils.getMethod(subscriber.getReceiverClass(),
			subscriber.getReceiverMethod(), receiverParameters);

	 } catch (ReflectionException e) {
	    LogCat.e(TAG,
			String.format("Failed to subscribe %s to message port.", subscriber.toString()), e);
	 }
	 assertNotNull("Receiving method of message port subscriber must not be null.", method);

	 // Method is valid, so we can create the subscription
	 subscribers.add(subscriber);
   }

   public void unsubscribe(MessagePortSubscriber subscriber) throws AssertionFailedError {
	 subscribers.remove(subscriber);
   }

   /**
    * Removes all currently active subscriptions.
    */
   public void clearSubscriptions() {
	 subscribers.clear();
   }

   /**
    * First the provided arguments are matched against the parameters defined by
    * this message port. If they match, the given message is forwarded to all
    * subscribers.
    * 
    * @param arguments
    *           the arguments to pass on to the subscribers.
    */
   public void send(Object... arguments) {
	 // Validate the arguments against the declared parameters
	 assertEquals("Number of declared parameters must match number of passed arguments.",
		  arguments.length, receiverParameters.length);

	 // Forward the arguments to subscribed receivers
	 for (MessagePortSubscriber subscriber : subscribers) {
	    Method method = null;
	    try {
		  method = ReflectionUtils.getMethod(subscriber.getReceiverClass(),
			   subscriber.getReceiverMethod(), receiverParameters);
	    } catch (ReflectionException e) {
		  LogCat.e(TAG, "Failed to get method " + subscriber.getReceiverMethod(), e);
	    }
	    assertNotNull("Failed to send message to " + subscriber.toString(), method);
	    // Invoke the method - we pass null as invoke target, since the method
	    // must be static
	    try {
		  ReflectionUtils.invoke(method, null, arguments);
	    } catch (ReflectionException e) {
		  LogCat.e(TAG, "Failed to invoke method " + subscriber.getReceiverMethod(), e);
		  // TODO throw a runtime exception?
	    }
	 }
   }

   /**
    * @return the number of subscribers.
    */
   public int getSubscriberCount() {
	 return subscribers.size();
   }
}
